﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace NetworkingLibrary.Clients.UDP
{
    public interface IUDPClient : IClient
    {
        IPAddress LocalAddress
        {
            get;
        }
        IPAddress RemoteAddress
        {
            get;
        }
        
        int LocalPort
        {
            get;
            set;
        }
        int HostPort
        {
            get;
        }
        IPAddress HostAddress
        {
            get;
            set;
        }

        bool Connect(int AHostPort, System.Net.IPAddress AHostAddress, bool ShouldStart = true, bool IsMulticast = false, int TimeToLive = 0);
        bool Start(int TimeToLive = 0);

        bool SendTo(IPAddress To, int ToPort, byte[] Data);
        bool SendTo(IPAddress To, int ToPort, Messages.MessageObject TheMessage);


        Utilities.UDP.UDPMessagingMode UDPMode
        {
            get;
            set;
        }
    }
    public class UDPClient : IUDPClient
    {
        private int maxFlowRate = 20;
        public int MaxFlowRate
        {
            get
            {
                return maxFlowRate;
            }
            set
            {
                maxFlowRate = value;
            }
        }
        private DateTime lastSendTime = new DateTime();
        public DateTime LastSendTime
        {
            get
            {
                return lastSendTime;
            }
            set
            {
                lastSendTime = value;
            }
        }
        
        protected int bufferSize = 65000;
        public int BufferSize
        {
            get
            {
                return bufferSize;
            }
            set
            {
                bufferSize = value;
            }
        }
        
        protected bool terminating = false;
        public bool Terminating
        {
            get
            {
                return terminating;
            }
            set
            {
                if (value)
                {
                    terminating = value;
                }
            }
        }

        private Guid id = Guid.NewGuid();
        public Guid ID
        {
            get
            {
                return id;
            }
            set
            {
                id = value;
            }
        }

        private bool connected = false;
        public bool Connected
        {
            get
            {
                return connected;
            }
        }
        
        //Events have to be overriden so that Invoke can be called on them
        public virtual event Events.ClientConnectDelegate OnConnect = delegate { };
        public virtual event Events.ClientDisconnectDelegate OnDisconnect = delegate { };
        public virtual event Events.MessageReceivedDelegate OnMessageReceived = delegate { };
        public virtual event Events.ErrorDelegate OnError = delegate { };
        
        private Socket theSocket;
        internal Socket TheSocket
        {
            get
            {
                return theSocket;
            }
        }

        public IPAddress LocalAddress
        {
            get
            {
                return Dns.GetHostAddresses(Dns.GetHostName()).First();
            }
        }
        public IPAddress RemoteAddress
        {
            get
            {
                return HostAddress ?? LocalAddress;
            }
        }
        
        private AsyncCallback ReceiveCallback;

        private EndPoint AnyEndPoint = (EndPoint)(new IPEndPoint(IPAddress.Any, 0));
        
        public int LocalPort
        {
            get;
            set;
        }
        private int hostPort = -1;
        public int HostPort
        {
            get
            {
                return hostPort;
            }
        }
        public IPAddress HostAddress
        {
            get;
            set;
        }

        private ManualResetEvent WaitForConnectedResponse = new ManualResetEvent(false);

        private bool Multicast = false;
        private bool MulticastCanRead = false;

        private Utilities.UDP.UDPMessagingMode udpMode = Utilities.UDP.UDPMessagingMode.UnreliableUnordered;
        public Utilities.UDP.UDPMessagingMode UDPMode
        {
            get
            {
                return udpMode;
            }
            set
            {
                if (value != Utilities.UDP.UDPMessagingMode.NotUDP)
                {
                    udpMode = value;
                }
            }
        }

        public UDPClient(int ALocalPort)
        {
            LocalPort = ALocalPort;
            OnError += new Events.ErrorDelegate(OnInternalError);
            OnMessageReceived += new Events.MessageReceivedDelegate(UDPClient_OnMessageReceived);
        }
        public UDPClient(int ALocalPort, Events.MessageReceivedDelegate OnAMessageReceived)
        {
            LocalPort = ALocalPort;
            OnError += new Events.ErrorDelegate(OnInternalError);
            OnMessageReceived += OnAMessageReceived;
            OnMessageReceived += new Events.MessageReceivedDelegate(UDPClient_OnMessageReceived);
        }
                
        private void SetUpSocket(int APort, bool IsMulticast, bool WillRead, IPAddress AnAddress, int TimeToLive = 0)
        {
            if (theSocket == null)
            {
                theSocket = new Socket(AnAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

                if (WillRead)
                {
                    if (IsMulticast)
                    {
                        IPEndPoint GroupEndPoint = new IPEndPoint(IPAddress.Any, HostPort);
                        theSocket.Bind(GroupEndPoint);
                    }
                    else
                    {
                        AnyEndPoint = new IPEndPoint(IPAddress.Any, APort);
                        if (AnAddress.AddressFamily == AddressFamily.InterNetworkV6)
                        {
                            ((IPEndPoint)AnyEndPoint).Address = IPAddress.IPv6Any;
                        }
                        theSocket.Bind(AnyEndPoint);
                    }
                }

                theSocket.Blocking = false;
                theSocket.EnableBroadcast = true;

                if (IsMulticast)
                {
                    SocketOptionLevel TheLevel = HostAddress.AddressFamily == AddressFamily.InterNetworkV6 ? SocketOptionLevel.IPv6 : SocketOptionLevel.IP;
                    theSocket.SetSocketOption(TheLevel, SocketOptionName.AddMembership, new MulticastOption(HostAddress, HostAddress.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any));
                    theSocket.SetSocketOption(TheLevel, SocketOptionName.MulticastTimeToLive, TimeToLive);
                }
            }
        }

        private void OnInternalError(Events.ErrorEventArgs e)
        {
            if (!Connected)
            {
                OnDisconnect.Invoke(new Events.ClientDisconnectEventArgs(this));
            }
        }

        public bool Connect(int AHostPort, System.Net.IPAddress AHostAddress, bool ShouldStart = true)
        {
            return Connect(AHostPort, AHostAddress, ShouldStart, false, 0);
        }
        public bool Connect(int AHostPort, System.Net.IPAddress AHostAddress, bool ShouldStart = true, bool IsMulticast = false, int TimeToLive = 0)
        {
            try
            {
                Terminating = false;
                HostAddress = AHostAddress;
                hostPort = AHostPort;

                if (IsMulticast)
                {
                    Multicast = true;
                    MulticastCanRead = ShouldStart;

                    SetUpSocket(HostPort, true, ShouldStart, HostAddress, TimeToLive);
                }
                else
                {
                    SetUpSocket(LocalPort, false, ShouldStart, LocalAddress);
                }

                if (ShouldStart)
                {
                    if (Start() && SendTo(HostAddress, HostPort, new Messages.UDP.Requests.ConnectRequestMessage(LocalAddress, LocalPort)))
                    {
                        WaitForConnectedResponse.WaitOne(10000);

                        if (Connected)
                        {
                            OnConnect.Invoke(new Events.ClientConnectEventArgs(this));
                            return true;
                        }
                        else
                        {
                            return false;
                        }

                    }
                }
                else if(SendTo(HostAddress, HostPort, new Messages.UDP.Requests.ConnectRequestMessage(LocalAddress, LocalPort)))
                {
                    return connected = true;
                }
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return false;
        }
        public bool Disconnect(bool IgnoreSocketState = false)
        {
            try
            {
                if (Connected)
                {
                    KillTimers();

                    if (Multicast)
                    {
                        SocketOptionLevel TheLevel = HostAddress.AddressFamily == AddressFamily.InterNetworkV6 ? SocketOptionLevel.IPv6 : SocketOptionLevel.IP;
                        theSocket.SetSocketOption(TheLevel, SocketOptionName.DropMembership, new MulticastOption(HostAddress));
                    }

                    bool OK = true;
                    if (!Multicast)
                    {
                        OK = Send(new Messages.UDP.DisconnectMessage(ID));
                    }

                    theSocket.Close();

                    connected = false;
                    OnDisconnect.Invoke(new Events.ClientDisconnectEventArgs(this));

                    return OK;
                }
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return theSocket == null ? true : !theSocket.Connected;
        }

        public bool Start()
        {
            return Start(0);
        }
        public bool Start(int TimeToLive = 0)
        {
            try
            {
                if (!Multicast || (Multicast && MulticastCanRead))
                {
                    if (!Multicast)
                    {
                        SetUpSocket(LocalPort, false, true, LocalAddress);
                    }
                    else
                    {
                        SetUpSocket(HostPort, true, true, HostAddress, TimeToLive);
                    }

                    ReceiveCallback = new AsyncCallback(Read);
                    byte[] TheBuffer = new byte[BufferSize];
                    theSocket.BeginReceive(TheBuffer, 0, TheBuffer.Length, SocketFlags.None, ReceiveCallback, TheBuffer);
                    return true;
                }
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return false;
        }
        public bool Stop()
        {
            try
            {
                KillTimers();

                Terminating = true;
                if (Multicast)
                {
                    return Disconnect();
                }
                else
                {                    
                    return true;
                }
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return false;
        }

        private void KillTimers()
        {
            try
            {
                if (CheckReceivedMessagesTimer != null)
                {
                    CheckReceivedMessagesTimer.Change(Timeout.Infinite, Timeout.Infinite);
                    CheckReceivedMessagesTimer = null;
                }
                if (CheckSentMessagesTimer != null)
                {
                    CheckSentMessagesTimer.Change(Timeout.Infinite, Timeout.Infinite);
                    CheckSentMessagesTimer = null;
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
        }

        private Dictionary<IPAddress, List<Messages.MessageObject>> MessagesReceived = new Dictionary<IPAddress, List<Messages.MessageObject>>();
        private Dictionary<IPAddress, int> LastReceivedNums = new Dictionary<IPAddress, int>();
        private Timer CheckReceivedMessagesTimer = null;
        private int receiveCheckSpeed = 100;
        public int ReceiveCheckIntervalMilliseconds
        {
            get
            {
                return receiveCheckSpeed;
            }
            set
            {
                receiveCheckSpeed = value;
            }
        }

        private void Read(IAsyncResult Result)
        {
            try
            {
                int BytesRead = theSocket.EndReceive(Result);
                
                if (!Terminating)
                {
                    byte[] TheBuffer = new byte[BufferSize];
                    theSocket.BeginReceive(TheBuffer, 0, TheBuffer.Length, SocketFlags.None, ReceiveCallback, TheBuffer);
                }

                if (BytesRead > 0)
                {
                    List<byte[]> AllMessages = Utilities.Utilities.GetAllMessages((byte[])(Result.AsyncState));
                    for (int i = 0; i < AllMessages.Count; i++)
                    {
                        Messages.MessageObject TheMessageObject = (Messages.MessageObject)(Utilities.Utilities.FromBytes(AllMessages[i]));
                        Type MessageType = TheMessageObject.GetType();

                        bool OK = true;

                        if (UDPMode == Utilities.UDP.UDPMessagingMode.UnreliableSequenced)
                        {
                            if (TheMessageObject.SentTime <= LastReceivedMessageTime)
                            {
                                OK = false;
                            }
                            else
                            {
                                LastReceivedMessageTime = TheMessageObject.SentTime;
                            }
                        }
                        else if (UDPMode == Utilities.UDP.UDPMessagingMode.ReliableUnordered || UDPMode == Utilities.UDP.UDPMessagingMode.ReliableSequenced || UDPMode == Utilities.UDP.UDPMessagingMode.ReliableOrdered)
                        {
                            if (MessageType != typeof(Messages.UDP.Wrapping.ReceiptMessage))
                            {
                                SendTo(TheMessageObject.UDPFromAddress, TheMessageObject.UDPFromPort, new Messages.UDP.Wrapping.ReceiptMessage(TheMessageObject.UDPMessageID));
                            }
                            else
                            {
                                OK = false;
                                Messages.MessageObject TheSentMessage = GetSentMessageByID(((Messages.UDP.Wrapping.ReceiptMessage)TheMessageObject).MessageID);
                                if (TheSentMessage != null)
                                {
                                    SentMessages.Remove(TheSentMessage);
                                }
                            }

                            if (OK)
                            {
                                if (UDPMode == Utilities.UDP.UDPMessagingMode.ReliableSequenced)
                                {
                                    if (TheMessageObject.SentTime <= LastReceivedMessageTime)
                                    {
                                        OK = false;
                                    }
                                    else
                                    {
                                        LastReceivedMessageTime = TheMessageObject.SentTime;
                                    }
                                }
                                else if (UDPMode == Utilities.UDP.UDPMessagingMode.ReliableOrdered)
                                {
                                    OK = false;

                                    if (!LastReceivedNums.ContainsKey(TheMessageObject.UDPFromAddress))
                                    {
                                        LastReceivedNums.Add(TheMessageObject.UDPFromAddress, 0);
                                        OK = true;
                                    }
                                    else
                                    {
                                        if (!MessagesReceived.ContainsKey(TheMessageObject.UDPFromAddress))
                                        {
                                            MessagesReceived.Add(TheMessageObject.UDPFromAddress, new List<Messages.MessageObject>());
                                        }

                                        bool IsNextMessage = LastReceivedNums[TheMessageObject.UDPFromAddress] == TheMessageObject.UDPMessageNum - 1;
                                        if (IsNextMessage)
                                        {
                                            if (MessagesReceived[TheMessageObject.UDPFromAddress].Count == 0)
                                            {
                                                OK = true;
                                                LastReceivedNums[TheMessageObject.UDPFromAddress] = TheMessageObject.UDPMessageNum;
                                            }
                                            else
                                            {
                                                MessagesReceived[TheMessageObject.UDPFromAddress].Add(TheMessageObject);
                                                InvokeReceievedNextMessage(false);
                                            }
                                        }
                                        else
                                        {
                                            MessagesReceived[TheMessageObject.UDPFromAddress].Add(TheMessageObject);
                                        }
                                    }

                                    if (CheckReceivedMessagesTimer == null)
                                    {
                                        CheckReceivedMessagesTimer = new Timer(new TimerCallback(InvokeReceievedNextMessage), false, receiveCheckSpeed, receiveCheckSpeed);
                                    }
                                }
                            }
                            else
                            {
                                OK = false;
                            }
                        }

                        if (OK)
                        {
                            if (MessageType.GetType() == typeof(Messages.DisconnectMessage))
                            {
                                connected = false;
                                OnDisconnect.Invoke(new Events.ClientDisconnectEventArgs(this));
                            }
                            else if (MessageType == typeof(Messages.UDP.Responses.ConnectResponseMessage))
                            {
                                Messages.UDP.Responses.ConnectResponseMessage TheMessage = (Messages.UDP.Responses.ConnectResponseMessage)TheMessageObject;
                                connected = TheMessage.Connected;
                                ID = TheMessage.NewClientID;

                                WaitForConnectedResponse.Set();
                            }
                            else
                            {
                                OnMessageReceived.Invoke(new Events.MessageReceivedEventArgs(TheMessageObject, this) { AlreadyProcessed = true });
                            }
                        }
                    }
                }
            }
            catch
            {
            }
        }
        private void UDPClient_OnMessageReceived(Events.MessageReceivedEventArgs e)
        {
            try
            {
                if (!e.AlreadyProcessed)
                {
                    Messages.MessageObject TheMessageObject = e.TheMessage;
                    Type MessageType = TheMessageObject.GetType();

                    if (MessageType.GetType() == typeof(Messages.DisconnectMessage))
                    {
                        connected = false;
                        OnDisconnect.Invoke(new Events.ClientDisconnectEventArgs(this));
                    }
                    else if (MessageType == typeof(Messages.UDP.Responses.ConnectResponseMessage))
                    {
                        Messages.UDP.Responses.ConnectResponseMessage TheMessage = (Messages.UDP.Responses.ConnectResponseMessage)TheMessageObject;
                        connected = TheMessage.Connected;
                        ID = TheMessage.NewClientID;

                        WaitForConnectedResponse.Set();
                    }
                    else
                    {
                        OnMessageReceived.Invoke(new Events.MessageReceivedEventArgs(TheMessageObject, this) { AlreadyProcessed = true });
                    }
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
        }
        bool LoopingReceivedMessages = false;
        private void InvokeReceievedNextMessage(object Loop)
        {
            InvokeReceievedNextMessage((bool)Loop);
        }
        private void InvokeReceievedNextMessage(bool Loop)
        {
            if ((!LoopingReceivedMessages || Loop) && (MessagesReceived.Count > 0))
            {
                if (!Loop)
                {
                    LoopingReceivedMessages = true;
                }

                for (int i = 0; i < LastReceivedNums.Count; i++)
                {
                    KeyValuePair<IPAddress, int> ValuePair = LastReceivedNums.ElementAt(i);
                    if (MessagesReceived[ValuePair.Key].Count > 0)
                    {
                        MessagesReceived[ValuePair.Key] = MessagesReceived[ValuePair.Key].OrderBy(x => x.UDPMessageNum).ToList();
                        Messages.MessageObject CurrentMessage = MessagesReceived[ValuePair.Key][0];
                        OnMessageReceived.Invoke(new Events.MessageReceivedEventArgs(CurrentMessage, this));
                        if (LastReceivedNums[ValuePair.Key] < ValuePair.Value)
                        {
                            LastReceivedNums[ValuePair.Key] = ValuePair.Value;
                        }
                        MessagesReceived[ValuePair.Key].Remove(CurrentMessage);
                        if (MessagesReceived[ValuePair.Key].Count > 0)
                        {
                            InvokeReceievedNextMessage(true);
                        }
                    }
                }

                if (!Loop)
                {
                    LoopingReceivedMessages = false;
                }
            }
        }

        private List<Messages.MessageObject> SentMessages = new List<Messages.MessageObject>();
        private DateTime LastReceivedMessageTime = new DateTime();
        private Timer CheckSentMessagesTimer = null;
        private int LastSentNum = 0;
        private int sentCheckSpeed = 500;
        public int SentCheckSpeed
        {
            get
            {
                return sentCheckSpeed;
            }
            set
            {
                if (value > MaxFlowRate)
                {
                    sentCheckSpeed = value;
                }
            }
        }

        public bool Send(byte[] Data)
        {
            try
            {
                return Send(new Messages.DataMessage(Data));
            }
            catch(Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return false;
        }
        public bool Send(Messages.MessageObject TheMessage)
        {
            try
            {
                return SendTo(HostAddress, HostPort, TheMessage);
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return false;
        }
        public bool SendTo(IPAddress To, int ToPort, byte[] Data)
        {
            try
            {
                return SendTo(To, ToPort, new Messages.DataMessage(Data));
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return false;
        }
        public bool SendTo(IPAddress To, int ToPort, Messages.MessageObject TheMessage)
        {
            return SendTo(To, ToPort, TheMessage, false);
        }
        internal bool SendTo(IPAddress To, int ToPort, Messages.MessageObject TheMessage, bool FromCheckSentMessages = false)
        {
            try
            {
                Utilities.Utilities.CheckFlowRate(this);
                
                switch(UDPMode)
                {
                    case Utilities.UDP.UDPMessagingMode.ReliableOrdered:
                    case Utilities.UDP.UDPMessagingMode.ReliableSequenced:
                    case Utilities.UDP.UDPMessagingMode.ReliableUnordered:
                        Type MessageType = TheMessage.GetType();
                        if (!FromCheckSentMessages && (MessageType != typeof(Messages.UDP.Wrapping.ReceiptMessage)))
                        {
                            CheckSentMessages();
                            SentMessages.Add(TheMessage);

                            if (CheckSentMessagesTimer == null)
                            {
                                CheckSentMessagesTimer = new Timer(new TimerCallback(CheckSentMessages), null, sentCheckSpeed, sentCheckSpeed);
                            }
                            CheckSentMessagesTimer.Change(sentCheckSpeed, sentCheckSpeed);
                            TheMessage.UDPMessageNum = ++LastSentNum;
                        }
                        break;
                    case Utilities.UDP.UDPMessagingMode.UnreliableSequenced:
                        break;
                    case Utilities.UDP.UDPMessagingMode.UnreliableUnordered:
                        break;
                    default:
                        throw new ArgumentException("Unrecognised UDP message mode.");
                }

                TheMessage.UDPFromAddress = Multicast ? HostAddress : LocalAddress;
                TheMessage.UDPFromPort = Multicast ? HostPort : LocalPort;
                TheMessage.SentTime = DateTime.Now;
                
                if (Multicast)
                {
                    EndPoint GroupEndPoint = new IPEndPoint(HostAddress, HostPort);
                    return theSocket.SendTo(Utilities.Utilities.FormMessage(Utilities.Utilities.ToBytes(TheMessage)), SocketFlags.None, GroupEndPoint) > 0;
                }
                else
                {
                    return theSocket.SendTo(Utilities.Utilities.FormMessage(Utilities.Utilities.ToBytes(TheMessage)), new IPEndPoint(To, ToPort)) > 0;
                }
            }
            catch (System.Net.Sockets.SocketException ex)
            {
                if (ex.ErrorCode != 10053)
                {
                    OnError.Invoke(new Events.ErrorEventArgs(ex));
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return false;
        }

        private Messages.MessageObject GetSentMessageByID(Guid MessageID)
        {
            IEnumerable<Messages.MessageObject> PotMessages = (from Messages in SentMessages
                                                              where Messages.UDPMessageID == MessageID
                                                              select Messages);
            if (PotMessages.Count() > 0)
            {
                return PotMessages.First();
            }
            return null;
        }
        private void CheckSentMessages(object x = null)
        {
            try
            {
                if (SentMessages.Count > 0)
                {
                    List<Messages.MessageObject> UnreceievedMessages = (from Messages in SentMessages
                                                                        where (Messages.SentTime.AddMilliseconds(1000) <= DateTime.Now)
                                                                        select Messages).ToList();
                    for (int i = 0; i < UnreceievedMessages.Count; i++)
                    {
                        if (!SendTo(UnreceievedMessages[i].UDPFromAddress, UnreceievedMessages[i].UDPFromPort, UnreceievedMessages[i], true))
                        {
                            SentMessages.Remove(UnreceievedMessages[i]);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
        }
    }
}
